package jmine.tec.aom.util;

import java.lang.reflect.Method;

import jmine.tec.aom.domain.AbstractPropertyValue;
import jmine.tec.aom.domain.Propertized;
import jmine.tec.aom.domain.Property;
import jmine.tec.aom.domain.impl.SyntheticPropertyValue;
import jmine.tec.aom.exception.FillException;
import jmine.tec.utils.reflection.fast.FastMethodNoArgs;
import jmine.tec.utils.reflection.fast.FastMethodSingleArgument;
import jmine.tec.utils.reflection.fast.asm.FastMethodCompiler;
import jmine.tec.utils.reflection.fast.reflection.FastMethodImpl;

import org.hibernate.proxy.HibernateProxyHelper;

import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BaseDAOFactory;

/**
 * Provides utilitary methods for creating {@link AbstractPropertyValue}.
 * 
 * @author jrenaut
 */
public class PropertyValueUtils {

    private static final String GET = "get";

    private static final String SET = "set";

    private static final String IS = "is";

    private BaseDAOFactory daoFactory;

    private FastMethodCompiler methodCompiler;

    /**
     * @param property a {@link Property}
     * @param value a value
     * @return a new intance of the appropriate {@link AbstractPropertyValue} with its value set
     * @throws FillException if the value cannot be converted
     */
    public AbstractPropertyValue<?> createPropertyValue(Property property, String value) throws FillException {
        AbstractPropertyValue<?> apv = this.createPropertyValue(property);
        apv.setValueAsString(value);
        return apv;
    }

    /**
     * @param property a {@link Property}
     * @return a new intance of the appropriate {@link AbstractPropertyValue}
     */
    public AbstractPropertyValue<?> createPropertyValue(Property property) {
        BaseDAO<AbstractPropertyValue> dao = this.daoFactory.getGenericDAO(property.getPropertyType().getPropertyValueType());
        AbstractPropertyValue apv = dao.createBean();
        apv.setProperty(property);
        return apv;
    }

    /**
     * @param property a {@link Property}
     * @param target the target {@link Propertized}
     * @param getter the getter {@link Method}
     * @return a {@link SyntheticPropertyValue}
     */
    public SyntheticPropertyValue createSyntheticPropertyValue(Property property, Propertized target, Method getter) {
        Method setter = this.inferSetterMethod(getter, target);

        FastMethodNoArgs fastGetter = (FastMethodNoArgs) this.methodCompiler.compileAndLoad(new FastMethodImpl(getter));
        FastMethodSingleArgument fastSetter = (FastMethodSingleArgument) this.methodCompiler.compileAndLoad(new FastMethodImpl(setter));

        return new SyntheticPropertyValue(this, property, target, fastGetter, fastSetter);
    }

    /**
     * @param method a {@link Method}
     * @param target the target
     * @return the corresponding Java Bean compliant getter {@link Method}
     */
    private Method inferSetterMethod(Method method, Object target) {
        String getterName = method.getName();
        String setterName = null;
        if (getterName.startsWith(GET)) {
            setterName = getterName.replaceFirst(GET, SET);
        } else if (getterName.startsWith(IS)) {
            setterName = getterName.replaceFirst(IS, SET);
        } else {
            throw new IllegalArgumentException("Expected a JavaBean compliant getter (" + getterName + ")");
        }

        try {
            return this.getTargetClass(target).getMethod(setterName, method.getReturnType());
        } catch (SecurityException e) {
            throw new IllegalArgumentException("Could not retrieve (" + setterName + ")");
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Could not retrieve (" + setterName + ")");
        }
    }

    /**
     * @param target an object
     * @return the actual class of the target
     */
    private Class<?> getTargetClass(Object target) {
        return HibernateProxyHelper.getClassWithoutInitializingProxy(target);
    }

    /**
     * @param daoFactory the daoFactory to set
     */
    public void setDaoFactory(BaseDAOFactory daoFactory) {
        this.daoFactory = daoFactory;
    }

    /**
     * @param methodCompiler the methodCompiler to set
     */
    public void setMethodCompiler(FastMethodCompiler methodCompiler) {
        this.methodCompiler = methodCompiler;
    }

    /**
     * Extracts the {@link AbstractPropertyValue} for a given {@link Property} from a {@link Propertized} recursing through any parent
     * {@link Propertized}.
     * 
     * @param propertized a {@link Propertized}
     * @param property a {@link Property}
     * @return an {@link AbstractPropertyValue}
     */
    @SuppressWarnings("unchecked")
    public static AbstractPropertyValue<? super Object> extractPropertyValue(Propertized propertized, Property property) {
        if (propertized == null) {
            return null;
        }

        AbstractPropertyValue parentCv = extractPropertyValue(propertized.getParent(), property);
        AbstractPropertyValue cv = propertized.getPropertyValue(property);

        if (parentCv != null) {
            return parentCv.merge(cv);
        } else {
            return cv;
        }
    }

}
